﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;
using System.Security.Principal;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
using System.ServiceModel.Description;
using System.Text;
using PSE.Hosting.ServiceHoster.Business;
using PSE.Hosting.ServiceHoster.Configuration;
using PSE.Hosting.ServiceHoster.Logging;
using System.Threading;

namespace PSE.Hosting.ServiceHoster.Business
{
    /// <summary>
    /// This class loads a service assembly to the memory, 
    /// and allow to perform manage tasks to the services in the assembly.
    /// </summary>
    /// 
    internal class PackageServicesLoader : MarshalByRefObject
    {
        #region Fields

        private Assembly _assembly = null;
        private string _assemblyName = String.Empty;
        private string _codeBase = String.Empty;
        private Dictionary<string, ServiceHost> _hosts = new Dictionary<string, ServiceHost>();
        private Dictionary<string, ServiceInfoData> _services = new Dictionary<string, ServiceInfoData>();

        #endregion

        #region Public Members

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public override object InitializeLifetimeService()
        {
            return null;
        }

        /// <summary>
        /// Loads the specified assembly to the memory.
        /// </summary>
        /// <param name="assembly">The assembly name.</param>
        /// <param name="codeBase">The full assembly URL description.</param>
        public void LoadServicesAssembly(string codeBase)
        {
            _assembly = Assembly.LoadFile(codeBase);

            if (_assembly != null)
            {
                _assemblyName = _assembly.FullName;
                _codeBase = codeBase;
            }
        }

        /// <summary>
        /// Get the full assembly name. 
        /// (Also known as Display name).
        /// </summary>
        /// <returns>returns the display name of the assembly.</returns>
        public string GetAssemblyName()
        {
            return _assemblyName;
        }

        /// <summary>
        /// Gets a list of the all services in the assembly.
        /// </summary>
        /// <returns>A dictionary with the ServiceNames and a Flag, for each service, 
        /// that indicates whether is alive.</returns>
        public Dictionary<string, ServiceData> GetServices()
        {
            Dictionary<string, ServiceData> services = new Dictionary<string, ServiceData>();

            foreach (KeyValuePair<string, ServiceHost> value in _hosts)
            {
                ServiceData service = new ServiceData();
                ServiceHost host = value.Value;

                if (host == null)
                {
                    service.IsAlive = false;
                }
                else
                {
                    if (_services.ContainsKey(value.Key))
                    {
                        service.IsAlive = (host.State == CommunicationState.Opened || host.State == CommunicationState.Opening);
                        service.MexAddress = _services[value.Key].MexAddress;

                        string mexAddress = String.Empty;

                        ServiceEndpoint mexEndpoint = host.Description.Endpoints.Find(typeof(System.ServiceModel.Description.IMetadataExchange));

                        if (mexEndpoint != null && mexEndpoint.Address != null)
                            mexAddress = mexEndpoint.Address.ToString();

                        service.MexAddress = mexAddress;
                    }
                    else
                    {
                        service.IsAlive = false;
                    }
                }

                services.Add(value.Key, service);
            }
            return services;
        }

        /// <summary>
        /// Atualiza todas a sessões do arquivo de configuração
        /// </summary>
        public void RefreshSections()
        {
            System.Configuration.Configuration configuration = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);

            if (configuration != null)
            {
                foreach (ConfigurationSection section in configuration.Sections)
                {
                    ConfigurationManager.RefreshSection(section.SectionInformation.SectionName);
                }

                foreach (ConfigurationSectionGroup sectionGroup in configuration.SectionGroups)
                {
                    foreach (ConfigurationSection section in sectionGroup.Sections)
                    {
                        ConfigurationManager.RefreshSection(section.SectionInformation.SectionName);
                    }
                }
            }
        }

        /// <summary>
        /// Starts all the services in the assembly.
        /// </summary>
        /// <returns>A dictionaty with any exception that might occurs on each service starting.</returns>
        public Dictionary<string, Exception> StartAll()
        {
            const string servicesSection = "system.serviceModel/services";
            Dictionary<string, Exception> exceptions = new Dictionary<string, Exception>();
            ServicesSection section = null;

            try
            {
                System.Configuration.Configuration configuration = ConfigurationManager.OpenExeConfiguration(_codeBase);
                //ConfigurationManager.RefreshSection(servicesSection);
                section = configuration.GetSection(servicesSection) as ServicesSection;
                if (section.Services == null || section.Services.Count == 0)
                {
                    throw new NoServiceInAssemblyException("There are no services defined in assembly " + _assembly.CodeBase);
                }
            }
            catch (Exception ex)
            {
                exceptions.Add(_assemblyName, ex);
                return exceptions;
            }

            StringBuilder message = new StringBuilder();
            message.AppendFormat("Assembly {0} was loaded.", _assemblyName);
            message.AppendLine();
            message.Append("Services:");

            string serviceName;

            foreach (ServiceElement service in section.Services)
            {
                serviceName = service.Name;

                try
                {
                    if (!_hosts.ContainsKey(serviceName))
                    {
                        ServiceHost host = SetupServiceHost(serviceName);

                        _hosts.Add(serviceName, host);

                        // Armazena os endpoints criados para o serviço.
                        string metadataExchangeAddress = String.Empty;
                        List<string> endpoints = new List<string>();

                        foreach (ServiceEndpointElement endpoint in service.Endpoints)
                        {
                            endpoints.Add(endpoint.Address.ToString());
                            EndpointInfoData endpointData = new EndpointInfoData();
                            endpointData.Address = endpoint.Address.ToString();
                            endpointData.Binding = endpoint.Binding;
                            endpointData.BindingConfiguration = endpoint.BindingConfiguration;
                            endpointData.Contract = endpoint.Contract;
                        }

                        ServiceInfoData data = new ServiceInfoData();
                        data.Name = service.Name;
                        data.MexAddress = metadataExchangeAddress;

                        if (!_services.ContainsKey(serviceName))
                        {
                            _services.Add(serviceName, data);
                        }

                        message.AppendLine();
                        message.Append(service.Name);
                    }
                }
                catch (Exception ex)
                {
                    exceptions.Add(serviceName, ex);
                }
            }

            Logger.Log(PSE.Framework.Configuration.Components.SystemComponents.System,
                       PSE.Framework.Configuration.Components.SystemComponents.HosterService, 
                       message.ToString(), Logger.EntryType.Information);

            return exceptions;
        }

        /// <summary>
        /// Starts a service by its name.
        /// </summary>
        /// <param name="serviceName">The service name.</param>
        public void Start(string serviceName)
        {
            ServiceHost host;

            if (!_hosts.TryGetValue(serviceName, out host))
            {
                throw new ArgumentOutOfRangeException("serviceName");
            }
            if (host.State == CommunicationState.Opened || host.State == CommunicationState.Opening)
            {
                throw new InvalidOperationException(string.Format("Service {0} is already started.", serviceName));
            }
            if (host.State == CommunicationState.Faulted)
            {
                host.Abort();
            }
            host = SetupServiceHost(serviceName);
            _hosts.Remove(serviceName);
            _hosts.Add(serviceName, host);
        }

        /// <summary>
        /// Stops all the services in the assembly.
        /// </summary>
        /// <returns>A dictionaty with any exception that might occurs on each service stopping.</returns>
        public Dictionary<string, Exception> StopAll()
        {
            Dictionary<string, Exception> exceptions = new Dictionary<string, Exception>();

            foreach (KeyValuePair<string, ServiceHost> host in _hosts)
            {
                try
                {
                    Stop(host.Key);
                }
                catch (Exception ex)
                {
                    exceptions.Add(host.Key, ex);
                }
            }

            _services.Clear();
            _hosts.Clear();

            return exceptions;
        }

        /// <summary>
        /// Stops a service by its name.
        /// </summary>
        /// <param name="serviceName">The service name.</param>
        public void Stop(string serviceName)
        {
            ServiceHost host;
            
            if (!_hosts.TryGetValue(serviceName, out host))
            {
                throw new ArgumentOutOfRangeException("serviceName");
            }
            if (host.State == CommunicationState.Closed || host.State == CommunicationState.Closing)
            {
                throw new InvalidOperationException(string.Format("Service {0} is already stopped.", serviceName));
            }

            switch (host.State)
            {
                case CommunicationState.Faulted:
                    host.Abort();
                    break;
                case CommunicationState.Closed:
                case CommunicationState.Closing:
                    break;
                default:
                    host.Close();
                    break;
            }

            ((IDisposable)host).Dispose();
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Handles a transition to the Faulted state of an <see cref="System.ServiceModel.ServiceHost"/> object.
        /// </summary>
        /// <param name="sender">The ServiceHost object.</param>
        /// <param name="e">Event arguments.</param>
        private void OnHostFaulted(object sender, EventArgs e)
        {
            ServiceHost host = sender as ServiceHost;

            string serviceName = host.Description.ServiceType.FullName;

            Logger.Log(string.Format("Service {0} has faulted (Assembly: {1}).", serviceName, _assemblyName), Logger.EntryType.Warning);

            if (host != null && host.State == CommunicationState.Faulted)
            {
                host.Abort();
            }
        }

        /// <summary>
        /// Creates a new <see cref="System.ServiceModel.ServiceHost"/> object and puts it into the Opened state.
        /// </summary>
        /// <param name="serviceName">The service name.</param>
        /// <returns>The <see cref="System.ServiceModel.ServiceHost"/> created.</returns>
        private ServiceHost SetupServiceHost(string serviceName)
        {
            Type serviceType = _assembly.GetType(serviceName);

            ServiceHost host = null;

            try
            {
                host = new ServiceHost(serviceType);
            }
            catch (ArgumentNullException ex)
            {
                string message = "Could not find type (" + serviceName + ") in the following assembly: " + _assembly.FullName + "." + System.Environment.NewLine +
                                 "Verify if the name attribute is correct in the service element of config file. It must be the service name with the full namespace.";

                throw new ArgumentNullException(message, ex);
            }

            host.Faulted += new EventHandler(OnHostFaulted);
            host.Open();

            return host;
        }

        #endregion
    }
}
